package lang

import (
	"gvm/instructions/base"
	"gvm/native/basic"
	"gvm/rtda"
	"gvm/rtda/heap"
	"strings"
	"unsafe"
)

func RegisterClass() (string, []*basic.NaMethod) {
	return "java/lang/Class", []*basic.NaMethod{
		{
			"getPrimitiveClass",
			"(Ljava/lang/String;)Ljava/lang/Class;",
			getPrimitiveClass,
		}, {
			"getName0",
			"()Ljava/lang/String;",
			getName0,
		}, {
			"desiredAssertionStatus0",
			"(Ljava/lang/Class;)Z",
			desiredAssertionStatus0,
		}, {
			"forName0",
			"(Ljava/lang/String;ZLjava/lang/ClassLoader;Ljava/lang/Class;)Ljava/lang/Class;",
			forName0,
		}, {
			"getDeclaredFields0",
			"(Z)[Ljava/lang/reflect/Field;",
			getDeclaredFields0,
		}, {
			"isPrimitive",
			"()Z",
			isPrimitive,
		}, {
			"isAssignableFrom",
			"(Ljava/lang/Class;)Z",
			isAssignableFrom,
		}, {
			"isInterface",
			"()Z",
			isInterface,
		},
	}
}

// static native Class<?> getPrimitiveClass(String name);
func getPrimitiveClass(jFrame *rtda.JFrame) {
	nameObj := jFrame.LocalVars().GetRef(0)
	name := heap.GoString(nameObj)

	loader := jFrame.Method().Class().GetClassLoader()
	class := loader.LoadClass(name).JClass()
	jFrame.OperandStack().PushRef(class)
}

/*
// cache the name to reduce the number of calls into the VM
private transient String name;
private native String getName0();
*/
func getName0(jFrame *rtda.JFrame) {
	this := jFrame.LocalVars().GetThis()
	class := this.Extra().(*heap.Klass)

	name := class.Name()
	loader := class.GetClassLoader()
	jFrame.OperandStack().PushRef(heap.JString(&loader, name))
}

/*
// Retrieves the desired assertion status of this class from the VM
private static native boolean desiredAssertionStatus0(Class<?> clazz);
*/
func desiredAssertionStatus0(jFrame *rtda.JFrame) {
	jFrame.OperandStack().PushBoolean(false)
}

// private static native Class<?> forName0(String name, boolean initialize,
//
//	                                    ClassLoader loader,
//	                                    Class<?> caller)
//	throws ClassNotFoundException;
//
// (Ljava/lang/String;ZLjava/lang/ClassLoader;Ljava/lang/Class;)Ljava/lang/Class;
func forName0(jFrame *rtda.JFrame) {
	vars := jFrame.LocalVars()
	jName := vars.GetRef(0)
	initialize := vars.GetBoolean(1)
	//jLoader := vars.GetRef(2)

	goName := heap.GoString(jName)
	goName = strings.Replace(goName, ".", "/", -1)
	loader := jFrame.Method().Class().GetClassLoader()
	goClass := loader.LoadClass(goName)
	jClass := goClass.JClass()

	if initialize && !goClass.InitStarted() {
		// undo forName0
		thread := jFrame.JThread()
		jFrame.SetNextPC(thread.PC())
		// init class
		base.InitClass(thread, goClass)
	} else {
		stack := jFrame.OperandStack()
		stack.PushRef(jClass)
	}
}

/*
Field(Class<?> declaringClass,

	String name,
	Class<?> type,
	int modifiers,
	int slot,
	String signature,
	byte[] annotations)
*/
const _fieldConstructorDescriptor = "" +
	"(Ljava/lang/Class;" +
	"Ljava/lang/String;" +
	"Ljava/lang/Class;" +
	"II" +
	"Ljava/lang/String;" +
	"[B)V"

// private native Field[] getDeclaredFields0(boolean publicOnly);
// (Z)[Ljava/lang/reflect/Field;
func getDeclaredFields0(frame *rtda.JFrame) {
	vars := frame.LocalVars()
	classObj := vars.GetThis()
	publicOnly := vars.GetBoolean(1)

	class := classObj.Extra().(*heap.Klass)
	fields := class.GetFields(publicOnly)
	fieldCount := uint(len(fields))

	classLoader := frame.Method().Class().GetClassLoader()
	fieldClass := classLoader.LoadClass("java/lang/reflect/Field")
	fieldArr := fieldClass.ArrayClass().NewArray(fieldCount)

	stack := frame.OperandStack()
	stack.PushRef(fieldArr)

	if fieldCount > 0 {
		thread := frame.JThread()
		fieldObjs := fieldArr.Refs()
		fieldConstructor := fieldClass.GetConstructor(_fieldConstructorDescriptor)
		for i, goField := range fields {
			fieldObj := fieldClass.NewObject()
			fieldObj.SetExtra(goField)
			fieldObjs[i] = fieldObj

			ops := rtda.NewOperandStack(8)
			ops.PushRef(fieldObj)                                           // this
			ops.PushRef(classObj)                                           // declaringClass
			ops.PushRef(heap.JString(&classLoader, goField.Name()))         // name
			ops.PushRef(goField.Type().JClass())                            // type
			ops.PushInt(int32(goField.AccessFlags()))                       // modifiers
			ops.PushInt(int32(goField.SlotId()))                            // slot
			ops.PushRef(getSignatureStr(&classLoader, goField.Signature())) // signature
			ops.PushRef(toByteArr(&classLoader, goField.AnnotationData()))  // annotations

			shimFrame := rtda.NewShimFrame(thread, ops)
			thread.PushJFrame(shimFrame)

			// init fieldObj
			base.InvokeMethod(shimFrame, fieldConstructor)
		}
	}
}

func getSignatureStr(loader *heap.ClassLoader, signature string) *heap.Object {
	if signature != "" {
		return heap.JString(loader, signature)
	}
	return nil
}

// []byte => byte[]
func toByteArr(loader *heap.ClassLoader, goBytes []byte) *heap.Object {
	if goBytes != nil {
		jBytes := castUint8sToInt8s(goBytes)
		return heap.NewByteArray(loader, jBytes)
	}
	return nil
}

func castUint8sToInt8s(goBytes []byte) (jBytes []int8) {
	ptr := unsafe.Pointer(&goBytes)
	jBytes = *((*[]int8)(ptr))
	return
}

// public native boolean isPrimitive();
// ()Z
func isPrimitive(jFrame *rtda.JFrame) {
	vars := jFrame.LocalVars()
	this := vars.GetThis()
	class := this.Extra().(*heap.Klass)

	stack := jFrame.OperandStack()
	stack.PushBoolean(class.IsPrimitive())
}

// public native boolean isAssignableFrom(Class<?> cls);
// (Ljava/lang/Class;)Z
func isAssignableFrom(jFrame *rtda.JFrame) {
	vars := jFrame.LocalVars()
	this := vars.GetThis()
	cls := vars.GetRef(1)

	thisClass := this.Extra().(*heap.Klass)
	clsClass := cls.Extra().(*heap.Klass)
	ok := thisClass.IsAssignableFrom(clsClass)

	stack := jFrame.OperandStack()
	stack.PushBoolean(ok)
}

// public native boolean isInterface();
// ()Z
func isInterface(jFrame *rtda.JFrame) {
	vars := jFrame.LocalVars()
	this := vars.GetThis()
	class := this.Extra().(*heap.Klass)

	stack := jFrame.OperandStack()
	stack.PushBoolean(class.IsInterface())
}
